D:\a\csshw\csshw\src\daemon\mod.rs
Line | Count | Source |
1 | | //! Daemon imlementation |
2 | | |
3 | | #![deny(clippy::implicit_return)] |
4 | | #![allow(clippy::needless_return, clippy::doc_overindented_list_items)] |
5 | | #![warn(missing_docs)] |
6 | | |
7 | | use std::cmp::max; |
8 | | use std::{ |
9 | | io, |
10 | | sync::{Arc, Mutex}, |
11 | | time::Duration, |
12 | | }; |
13 | | use std::{thread, time}; |
14 | | |
15 | | use crate::get_console_window_handle; |
16 | | use crate::utils::config::{Cluster, DaemonConfig}; |
17 | | use crate::utils::debug::StringRepr; |
18 | | use crate::utils::windows::{clear_screen, set_console_color, WindowsApi}; |
19 | | use crate::{ |
20 | | serde::{serialization::serialize_input_record_0, SERIALIZED_INPUT_RECORD_0_LENGTH}, |
21 | | spawn_console_process, |
22 | | utils::{ |
23 | | constants::{PIPE_NAME, PKG_NAME}, |
24 | | windows::{ |
25 | | arrange_console, get_console_input_buffer, read_keyboard_input, |
26 | | set_console_border_color, |
27 | | }, |
28 | | }, |
29 | | WindowsSettingsDefaultTerminalApplicationGuard, |
30 | | }; |
31 | | use bracoxide::explode; |
32 | | use log::{debug, error, warn}; |
33 | | use tokio::sync::broadcast::error::TryRecvError; |
34 | | use tokio::{ |
35 | | net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions}, |
36 | | sync::broadcast::{self, Receiver, Sender}, |
37 | | task::JoinHandle, |
38 | | }; |
39 | | use windows::Win32::System::Console::{ |
40 | | CONSOLE_CHARACTER_ATTRIBUTES, INPUT_RECORD_0, LEFT_CTRL_PRESSED, RIGHT_CTRL_PRESSED, |
41 | | }; |
42 | | |
43 | | use windows::Win32::UI::Input::KeyboardAndMouse::{ |
44 | | VIRTUAL_KEY, VK_A, VK_C, VK_E, VK_ESCAPE, VK_H, VK_R, VK_T, |
45 | | }; |
46 | | use windows::Win32::UI::WindowsAndMessaging::{SW_RESTORE, SW_SHOWMINIMIZED}; |
47 | | use windows::Win32::{ |
48 | | Foundation::{COLORREF, HANDLE, HWND, STILL_ACTIVE}, |
49 | | System::{Console::ENABLE_PROCESSED_INPUT, Threading::PROCESS_QUERY_INFORMATION}, |
50 | | }; |
51 | | |
52 | | use self::workspace::WorkspaceArea; |
53 | | |
54 | | mod workspace; |
55 | | |
56 | | /// The capacity of the broadcast channel used |
57 | | /// to send the input records read from the console input buffer |
58 | | /// to the named pipe servers connected to each client in parallel. |
59 | | const SENDER_CAPACITY: usize = 1024 * 1024; |
60 | | |
61 | | /// Representation of a client |
62 | | #[derive(Clone)] |
63 | | struct Client { |
64 | | /// Hostname the client is connect to (or supposed to connect to). |
65 | | hostname: String, |
66 | | /// Window handle to the clients console window. |
67 | | window_handle: HWND, |
68 | | /// Process handle to the client process. |
69 | | process_handle: HANDLE, |
70 | | } |
71 | | |
72 | | unsafe impl Send for Client {} |
73 | | |
74 | | /// Hacky wrapper around a window handle. |
75 | | /// |
76 | | /// As we cannot implement foreign traits for foreign structs |
77 | | /// we introduce this wrapper to implement [Send] for [HWND]. |
78 | | #[derive(Debug, Eq)] |
79 | | struct HWNDWrapper { |
80 | | hwdn: HWND, |
81 | | } |
82 | | |
83 | | unsafe impl Send for HWNDWrapper {} |
84 | | |
85 | | impl PartialEq for HWNDWrapper { |
86 | | /// Returns whether to `HWNDWrapper` instances are equal or not |
87 | | /// based on the [HWND] they wrap. |
88 | 2 | fn eq(&self, other: &Self) -> bool { |
89 | 2 | return self.hwdn == other.hwdn; |
90 | 2 | } |
91 | | } |
92 | | |
93 | | /// Returns a window handle to the current console window. |
94 | | /// |
95 | | /// The [HWND] is wrapped in a `HWNDWrapper` so that |
96 | | /// we can pass it inbetween threads. |
97 | 0 | fn get_console_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper { |
98 | 0 | return HWNDWrapper { |
99 | 0 | hwdn: api.get_console_window(), |
100 | 0 | }; |
101 | 0 | } |
102 | | |
103 | | /// Returns a window handle to the foreground window. |
104 | | /// |
105 | | /// The [HWND] is wrapped in a `HWNDWrapper` so that |
106 | | /// we can pass it inbetween threads. |
107 | 0 | fn get_foreground_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper { |
108 | 0 | return HWNDWrapper { |
109 | 0 | hwdn: api.get_foreground_window(), |
110 | 0 | }; |
111 | 0 | } |
112 | | |
113 | | /// Enum of all possible control mode states. |
114 | | #[derive(PartialEq, Debug)] |
115 | | enum ControlModeState { |
116 | | /// Controle mode is inactive. |
117 | | Inactive, |
118 | | /// One of the keys required for the control mode key combination |
119 | | /// is currently being pressed. |
120 | | Initiated, |
121 | | /// All required keys for the control mode key combination were pressed |
122 | | /// and control mode is now active. |
123 | | /// |
124 | | /// Active control mode prevents any input records from being sent to clients. |
125 | | Active, |
126 | | } |
127 | | |
128 | | /// The daemon is responsible to launch a client for |
129 | | /// each host, positioning the client windows, forwarding |
130 | | /// input records to all clients and handling control mode. |
131 | | struct Daemon<'a> { |
132 | | /// A list of hostnames to connect to. |
133 | | hosts: Vec<String>, |
134 | | /// A username to use to connect to all clients. |
135 | | /// |
136 | | /// If it is empty the clients will use the SSH config to find an approriate |
137 | | /// username. |
138 | | username: Option<String>, |
139 | | /// Optional port used for all SSH connections. |
140 | | port: Option<u16>, |
141 | | /// The `DaemonConfig` that controls how the daemon console window looks like. |
142 | | config: &'a DaemonConfig, |
143 | | /// List of available cluster tags |
144 | | clusters: &'a [Cluster], |
145 | | /// The current control mode state. |
146 | | control_mode_state: ControlModeState, |
147 | | /// If debug mode is enabled on the daemon it will also be enabled on all |
148 | | /// clients. |
149 | | debug: bool, |
150 | | } |
151 | | |
152 | | impl<'a> Daemon<'a> { |
153 | | /// Launches all client windows and blocks on the main run loop. |
154 | | /// |
155 | | /// Sets up the daemon console by disabling processed input mode and applying |
156 | | /// the configured colors and dimensions. |
157 | | /// Once all client windows have successfully started the daemon console window |
158 | | /// is moved to the foreground and receives focus. |
159 | 0 | async fn launch<W: WindowsApi + Clone + 'static>(mut self, windows_api: &W) { |
160 | 0 | windows_api |
161 | 0 | .set_console_title(format!("{PKG_NAME} daemon").as_str()) |
162 | 0 | .unwrap(); |
163 | 0 | set_console_color( |
164 | 0 | windows_api, |
165 | 0 | CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color), |
166 | | ); |
167 | 0 | set_console_border_color(windows_api, COLORREF(0x000000FF)); |
168 | | |
169 | 0 | toggle_processed_input_mode(windows_api); // Disable processed input mode |
170 | | |
171 | | // Initialize the COM library so we can use UI automation |
172 | 0 | windows_api |
173 | 0 | .initialize_com_library(windows::Win32::System::Com::COINIT_MULTITHREADED) |
174 | 0 | .unwrap(); |
175 | | |
176 | 0 | let workspace_area = workspace::get_workspace_area(windows_api, self.config.height); |
177 | | |
178 | 0 | self.arrange_daemon_console(windows_api, &workspace_area); |
179 | | |
180 | | // Looks like on windows 10 re-arranging the console resets the console output buffer |
181 | 0 | set_console_color( |
182 | 0 | windows_api, |
183 | 0 | CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color), |
184 | | ); |
185 | | |
186 | 0 | let mut clients = Arc::new(Mutex::new( |
187 | 0 | launch_clients( |
188 | 0 | windows_api, |
189 | 0 | self.hosts.to_vec(), |
190 | 0 | &self.username, |
191 | 0 | self.port, |
192 | 0 | self.debug, |
193 | 0 | &workspace_area, |
194 | 0 | self.config.aspect_ratio_adjustement, |
195 | 0 | 0, |
196 | 0 | ) |
197 | 0 | .await, |
198 | | )); |
199 | | |
200 | | // Now that all clients started, focus the daemon console again. |
201 | 0 | let daemon_console = windows_api.get_console_window(); |
202 | 0 | let _ = windows_api.set_foreground_window(daemon_console); |
203 | 0 | let _ = windows_api.focus_window_with_automation(daemon_console); |
204 | | |
205 | 0 | self.print_instructions(windows_api); |
206 | 0 | self.run(windows_api, &mut clients, &workspace_area).await; |
207 | 0 | } |
208 | | |
209 | | /// The main run loop of the `daemon` subcommand. |
210 | | /// |
211 | | /// Opens a multi-producer, multi-consumer broadcasting channel used to |
212 | | /// send the read input records in parallel to the name pipe servers |
213 | | /// the clients are listening on. |
214 | | /// Spawns a background thread that waits for all clients to terminate |
215 | | /// and then stops the current process. |
216 | | /// Spawns a background thread that ensures the z-order of all client |
217 | | /// windows is in sync with the daemon window. |
218 | | /// I.e. if the daemon window is focussed, all clients should be moved to the foreground. |
219 | | /// |
220 | | /// The main loop consists of waiting for input records to read from the keyboard, |
221 | | /// sending them to all clients and handling control mode. |
222 | | /// |
223 | | /// # Arguments |
224 | | /// |
225 | | /// * `windows_api` - The Windows API implementation to use |
226 | | /// * `clients` - A thread safe mapping from the number |
227 | | /// a client console window was launched at |
228 | | /// in relation to the other client windows |
229 | | /// and the clients console window handle. |
230 | | /// * `workspace_area` - The available workspace area on the |
231 | | /// primary monitor minus the space occupied |
232 | | /// by the daemon console window. |
233 | 0 | async fn run<W: WindowsApi + Clone + 'static>( |
234 | 0 | &mut self, |
235 | 0 | windows_api: &W, |
236 | 0 | clients: &mut Arc<Mutex<Vec<Client>>>, |
237 | 0 | workspace_area: &workspace::WorkspaceArea, |
238 | 0 | ) { |
239 | 0 | let (sender, _) = |
240 | 0 | broadcast::channel::<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>(SENDER_CAPACITY); |
241 | | |
242 | 0 | let mut servers = Arc::new(Mutex::new(self.launch_named_pipe_servers(&sender))); |
243 | | |
244 | | // Monitor client processes |
245 | 0 | let clients_clone = Arc::clone(clients); |
246 | 0 | let windows_api_clone = windows_api.clone(); |
247 | 0 | tokio::spawn(async move { |
248 | | loop { |
249 | 0 | clients_clone.lock().unwrap().retain(|client| { |
250 | 0 | match windows_api_clone.get_exit_code(client.process_handle) { |
251 | 0 | Ok(exit_code) => return exit_code == STILL_ACTIVE.0 as u32, |
252 | 0 | Err(_) => return false, // Process handle is invalid, remove client |
253 | | } |
254 | 0 | }); |
255 | 0 | if clients_clone.lock().unwrap().is_empty() { |
256 | | // All clients have exited, exit the daemon as well |
257 | 0 | std::process::exit(0); |
258 | 0 | } |
259 | 0 | tokio::time::sleep(Duration::from_millis(5)).await; |
260 | | } |
261 | | }); |
262 | | |
263 | 0 | ensure_client_z_order_in_sync_with_daemon( |
264 | 0 | Arc::new(windows_api.clone()), |
265 | 0 | clients.to_owned(), |
266 | | ); |
267 | | |
268 | | loop { |
269 | 0 | self.handle_input_record( |
270 | 0 | windows_api, |
271 | 0 | &sender, |
272 | 0 | read_keyboard_input(windows_api), |
273 | 0 | clients, |
274 | 0 | workspace_area, |
275 | 0 | &mut servers, |
276 | 0 | ) |
277 | 0 | .await; |
278 | | } |
279 | | } |
280 | | |
281 | | /// Launch a named pipe server for each host in a dedicated thread. |
282 | | /// |
283 | | /// # Arguments |
284 | | /// |
285 | | /// * `sender` - The sender end of the broadcast channel through which |
286 | | /// the main thread will send the input records that are to |
287 | | /// be forwarded to the clients. |
288 | | /// |
289 | | /// # Returns |
290 | | /// |
291 | | /// Returns a list of [JoinHandle]s, one handle for each thread. |
292 | 0 | fn launch_named_pipe_servers( |
293 | 0 | &self, |
294 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
295 | 0 | ) -> Vec<JoinHandle<()>> { |
296 | 0 | let mut servers: Vec<JoinHandle<()>> = Vec::new(); |
297 | 0 | for _ in &self.hosts { |
298 | 0 | self.launch_named_pipe_server(&mut servers, sender); |
299 | 0 | } |
300 | 0 | return servers; |
301 | 0 | } |
302 | | |
303 | | /// Launch a named pipe server in a dedicated thread. |
304 | | /// |
305 | | /// # Arguments |
306 | | /// |
307 | | /// * `servers` - A list of [JoinHandle]s to which the join handle for |
308 | | /// the new thread will be added. |
309 | | /// * `sender` - The sender end of the broadcast channel through which |
310 | | /// the main thread will send the input records that are to |
311 | | /// be forwarded to the clients. |
312 | 0 | fn launch_named_pipe_server( |
313 | 0 | &self, |
314 | 0 | servers: &mut Vec<JoinHandle<()>>, |
315 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
316 | 0 | ) { |
317 | 0 | let named_pipe_server = ServerOptions::new() |
318 | 0 | .access_outbound(true) |
319 | 0 | .pipe_mode(PipeMode::Message) |
320 | 0 | .create(PIPE_NAME) |
321 | 0 | .unwrap_or_else(|err| { |
322 | 0 | error!("{}", err); |
323 | 0 | panic!("Failed to create named pipe server",) |
324 | | }); |
325 | 0 | let mut receiver = sender.subscribe(); |
326 | 0 | servers.push(tokio::spawn(async move { |
327 | 0 | named_pipe_server_routine(named_pipe_server, &mut receiver).await; |
328 | 0 | })); |
329 | 0 | } |
330 | | |
331 | | /// Handle the given input record. |
332 | | /// |
333 | | /// Input records are being forwarded to all clients. |
334 | | /// If a sequence of input records matches the control mode |
335 | | /// key combination, forwarding is temporarily interrupted, |
336 | | /// until control mode is exited. |
337 | | /// |
338 | | /// # Arguments |
339 | | /// |
340 | | /// * `sender` - The sender end of the broadcast channel |
341 | | /// through which we will send the input records |
342 | | /// that are being forwarded to the clients |
343 | | /// by the named pipe servers (`servers`). |
344 | | /// * `input_record` - The [INPUT_RECORD_0].`KeyEvent` read from the |
345 | | /// console input buffer. |
346 | | /// * `clients` - A thread safe mapping from the number |
347 | | /// a client console window was launched at |
348 | | /// in relation to the other client windows |
349 | | /// and the clients console window handle. |
350 | | /// The mapping will be extended if additional clients |
351 | | /// are being added through control mode `[c]reate window(s)`. |
352 | | /// * `workspace_area` - The available workspace area on the |
353 | | /// primary monitor minus the space occupied |
354 | | /// by the daemon console window. |
355 | | /// * `servers` - A thread safe list of [JoinHandle]s, |
356 | | /// one handle for each named pipe server background thread. |
357 | | /// The list will be extended if additional clients are being added |
358 | | /// through control mode `[c]reate window(s)`. |
359 | 0 | async fn handle_input_record<W: WindowsApi + Clone + 'static>( |
360 | 0 | &mut self, |
361 | 0 | windows_api: &W, |
362 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
363 | 0 | input_record: INPUT_RECORD_0, |
364 | 0 | clients: &mut Arc<Mutex<Vec<Client>>>, |
365 | 0 | workspace_area: &workspace::WorkspaceArea, |
366 | 0 | servers: &mut Arc<Mutex<Vec<JoinHandle<()>>>>, |
367 | 0 | ) { |
368 | 0 | if self.control_mode_is_active(windows_api, input_record) { |
369 | 0 | if self.control_mode_state == ControlModeState::Initiated { |
370 | 0 | clear_screen(windows_api); |
371 | 0 | println!("Control Mode (Esc to exit)"); |
372 | 0 | println!("[c]reate window(s), [r]etile, copy active [h]ostname(s)"); |
373 | 0 | self.control_mode_state = ControlModeState::Active; |
374 | 0 | return; |
375 | 0 | } |
376 | 0 | let key_event = unsafe { input_record.KeyEvent }; |
377 | 0 | if !key_event.bKeyDown.as_bool() { |
378 | 0 | return; |
379 | 0 | } |
380 | 0 | match ( |
381 | 0 | VIRTUAL_KEY(key_event.wVirtualKeyCode), |
382 | 0 | key_event.dwControlKeyState, |
383 | 0 | ) { |
384 | 0 | (VK_R, 0) => { |
385 | 0 | self.rearrange_client_windows( |
386 | 0 | windows_api, |
387 | 0 | &clients.lock().unwrap(), |
388 | 0 | workspace_area, |
389 | 0 | ); |
390 | 0 | self.arrange_daemon_console(windows_api, workspace_area); |
391 | 0 | } |
392 | 0 | (VK_E, 0) => { |
393 | 0 | // TODO: Select windows |
394 | 0 | } |
395 | 0 | (VK_T, 0) => { |
396 | 0 | // TODO: trigger input on selected windows |
397 | 0 | } |
398 | | (VK_C, 0) => { |
399 | 0 | clear_screen(windows_api); |
400 | | // TODO: make ESC abort |
401 | 0 | println!("Hostname(s) or cluster tag(s): (leave empty to abort)"); |
402 | 0 | toggle_processed_input_mode(windows_api); // As it was disabled before, this enables it again |
403 | 0 | let mut hostnames = String::new(); |
404 | 0 | match io::stdin().read_line(&mut hostnames) { |
405 | 0 | Ok(2) => { |
406 | 0 | // Empty input (only newline '\n') |
407 | 0 | } |
408 | | Ok(_) => { |
409 | 0 | let number_of_existing_clients = clients.lock().unwrap().len(); |
410 | 0 | let new_clients = launch_clients( |
411 | 0 | windows_api, |
412 | 0 | resolve_cluster_tags( |
413 | 0 | hostnames.split(' ').map(|x| return x.trim()).collect(), |
414 | 0 | self.clusters, |
415 | | ) |
416 | 0 | .into_iter() |
417 | 0 | .map(|x| return x.to_owned()) |
418 | 0 | .collect(), |
419 | 0 | &self.username, |
420 | 0 | self.port, |
421 | 0 | self.debug, |
422 | 0 | workspace_area, |
423 | 0 | self.config.aspect_ratio_adjustement, |
424 | 0 | number_of_existing_clients, |
425 | | ) |
426 | 0 | .await; |
427 | 0 | for client in new_clients.into_iter() { |
428 | 0 | clients.lock().unwrap().push(client); |
429 | 0 | self.launch_named_pipe_server(&mut servers.lock().unwrap(), sender); |
430 | 0 | } |
431 | | } |
432 | 0 | Err(error) => { |
433 | 0 | error!("{error}"); |
434 | | } |
435 | | } |
436 | 0 | toggle_processed_input_mode(windows_api); // Re-disable processed input mode. |
437 | 0 | self.rearrange_client_windows( |
438 | 0 | windows_api, |
439 | 0 | &clients.lock().unwrap(), |
440 | 0 | workspace_area, |
441 | | ); |
442 | 0 | self.arrange_daemon_console(windows_api, workspace_area); |
443 | | // Focus the daemon console again. |
444 | 0 | let daemon_window = windows_api.get_console_window(); |
445 | 0 | let _ = windows_api.set_foreground_window(daemon_window); |
446 | 0 | let _ = windows_api.focus_window_with_automation(daemon_window); |
447 | 0 | self.quit_control_mode(windows_api); |
448 | | } |
449 | | (VK_H, 0) => { |
450 | 0 | let mut active_hostnames: Vec<String> = vec![]; |
451 | 0 | for client in clients.lock().unwrap().iter() { |
452 | 0 | if windows_api.is_window(client.window_handle) { |
453 | 0 | active_hostnames.push(client.hostname.clone()); |
454 | 0 | } |
455 | | } |
456 | 0 | cli_clipboard::set_contents(active_hostnames.join(" ")).unwrap(); |
457 | 0 | self.quit_control_mode(windows_api); |
458 | | } |
459 | 0 | _ => {} |
460 | | } |
461 | 0 | return; |
462 | 0 | } |
463 | 0 | let error_handler = |err| { |
464 | 0 | error!("{}", err); |
465 | 0 | panic!( |
466 | 0 | "Failed to serialize input recored `{}`", |
467 | 0 | input_record.string_repr() |
468 | | ) |
469 | | }; |
470 | 0 | match sender.send( |
471 | 0 | serialize_input_record_0(&input_record)[..] |
472 | 0 | .try_into() |
473 | 0 | .unwrap_or_else(error_handler), |
474 | 0 | ) { |
475 | 0 | Ok(_) => {} |
476 | 0 | Err(_) => { |
477 | 0 | thread::sleep(time::Duration::from_nanos(1)); |
478 | 0 | } |
479 | | } |
480 | 0 | } |
481 | | |
482 | | /// Returns whether control mode is active or not given the input_record. |
483 | | /// |
484 | | /// For control mode to be active this function needs to be called |
485 | | /// multiple times, as a key press translates to an input record and |
486 | | /// the key combination that activates control mode has 2 keys: |
487 | | /// `Ctrl + A`. |
488 | | /// The current control mode state is stored in `self.control_mode_state`. |
489 | | /// |
490 | | /// # Arguments |
491 | | /// |
492 | | /// * `windows_api` - The Windows API implementation to use |
493 | | /// * `input_record` - A KeyEvent input record. |
494 | | /// |
495 | | /// # Returns |
496 | | /// |
497 | | /// Whether or not control mode is active. |
498 | 0 | fn control_mode_is_active<W: WindowsApi>( |
499 | 0 | &mut self, |
500 | 0 | windows_api: &W, |
501 | 0 | input_record: INPUT_RECORD_0, |
502 | 0 | ) -> bool { |
503 | 0 | let key_event = unsafe { input_record.KeyEvent }; |
504 | 0 | if self.control_mode_state == ControlModeState::Active { |
505 | 0 | if key_event.wVirtualKeyCode == VK_ESCAPE.0 { |
506 | 0 | self.quit_control_mode(windows_api); |
507 | 0 | return false; |
508 | 0 | } |
509 | 0 | return true; |
510 | 0 | } |
511 | 0 | if (key_event.dwControlKeyState & LEFT_CTRL_PRESSED >= 1 |
512 | 0 | || key_event.dwControlKeyState & RIGHT_CTRL_PRESSED >= 1) |
513 | 0 | && key_event.wVirtualKeyCode == VK_A.0 |
514 | | { |
515 | 0 | self.control_mode_state = ControlModeState::Initiated; |
516 | 0 | return true; |
517 | 0 | } |
518 | 0 | return false; |
519 | 0 | } |
520 | | |
521 | | /// Prints the default daemon instructions to the daemon console and |
522 | | /// sets `self.control_mode_state` to inactive. |
523 | 0 | fn quit_control_mode<W: WindowsApi>(&mut self, windows_api: &W) { |
524 | 0 | self.print_instructions(windows_api); |
525 | 0 | self.control_mode_state = ControlModeState::Inactive; |
526 | 0 | } |
527 | | |
528 | | /// Clears the console screen and prints the default daemon instructions. |
529 | 0 | fn print_instructions<W: WindowsApi>(&self, windows_api: &W) { |
530 | 0 | clear_screen(windows_api); |
531 | 0 | println!("Input to terminal: (Ctrl-A to enter control mode)"); |
532 | 0 | } |
533 | | |
534 | | /// Iterates over all still open client windows and re-arranges them |
535 | | /// on the screen based on the aspect ration adjustment daemon configuration. |
536 | | /// |
537 | | /// Client windows will be re-sized and re-positioned. |
538 | | /// |
539 | | /// # Arguments |
540 | | /// |
541 | | /// * `windows_api` - The Windows API implementation to use |
542 | | /// * `clients` - A thread safe mapping from the number |
543 | | /// a client console window was launched at |
544 | | /// in relation to the other client windows |
545 | | /// and the clients console window handle. |
546 | | /// The number is relevant to determine the |
547 | | /// position on the screen the window should |
548 | | /// be placed at. |
549 | | /// * `workspace_area` - The available workspace area on the |
550 | | /// primary monitor minus the space occupied |
551 | | /// by the daemon console window. |
552 | 0 | fn rearrange_client_windows<W: WindowsApi>( |
553 | 0 | &self, |
554 | 0 | windows_api: &W, |
555 | 0 | clients: &[Client], |
556 | 0 | workspace_area: &workspace::WorkspaceArea, |
557 | 0 | ) { |
558 | 0 | let mut valid_clients = Vec::new(); |
559 | 0 | for client in clients.iter() { |
560 | 0 | let exit_code = match windows_api.get_exit_code(client.process_handle) { |
561 | 0 | Ok(code) => code, |
562 | 0 | Err(_) => continue, // Process handle is invalid, skip client |
563 | | }; |
564 | 0 | if exit_code == STILL_ACTIVE.0 as u32 && windows_api.is_window(client.window_handle) { |
565 | 0 | valid_clients.push(client); |
566 | 0 | } |
567 | | } |
568 | 0 | for (index, client) in valid_clients.iter().enumerate() { |
569 | 0 | arrange_client_window( |
570 | 0 | windows_api, |
571 | 0 | &client.window_handle, |
572 | 0 | workspace_area, |
573 | 0 | index, |
574 | 0 | valid_clients.len(), |
575 | 0 | self.config.aspect_ratio_adjustement, |
576 | | ) |
577 | | } |
578 | 0 | } |
579 | | |
580 | | /// Re-sizes and re-positions the daemon console window on the screen |
581 | | /// based on the daemon height configuration. |
582 | | /// |
583 | | /// # Arguments |
584 | | /// |
585 | | /// * `windows_api` - The Windows API implementation to use |
586 | | /// * `workspace_area` - The available workspace area on the |
587 | | /// primary monitor minus the space occupied |
588 | | /// by the daemon console window. |
589 | 0 | fn arrange_daemon_console<W: WindowsApi>( |
590 | 0 | &self, |
591 | 0 | windows_api: &W, |
592 | 0 | workspace_area: &WorkspaceArea, |
593 | 0 | ) { |
594 | 0 | let (x, y, width, height) = get_console_rect( |
595 | 0 | 0, |
596 | 0 | workspace_area.height, |
597 | 0 | workspace_area.width - (workspace_area.x_fixed_frame + workspace_area.x_size_frame), |
598 | 0 | self.config.height, |
599 | 0 | workspace_area, |
600 | 0 | ); |
601 | 0 | arrange_console(windows_api, x, y, width, height); |
602 | 0 | } |
603 | | } |
604 | | |
605 | | /// The processed console input mode controls whether special key combinations |
606 | | /// such as `Ctrl + c` or `Ctrl + BREAK` receive special handling or are treated |
607 | | /// as simple key presses. |
608 | | /// |
609 | | /// By default processed input mode is enabled, meaning `Ctrl + c` is treated as |
610 | | /// a signal, not key presses. |
611 | | /// |
612 | | /// <https://learn.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals> |
613 | | /// |
614 | | /// # Arguments |
615 | | /// |
616 | | /// * `windows_api` - The Windows API implementation to use |
617 | 0 | fn toggle_processed_input_mode<W: WindowsApi>(windows_api: &W) { |
618 | 0 | let handle = get_console_input_buffer(); |
619 | 0 | let mode = windows_api.get_console_mode(handle).unwrap(); |
620 | 0 | let new_mode = windows::Win32::System::Console::CONSOLE_MODE(mode.0 ^ ENABLE_PROCESSED_INPUT.0); |
621 | 0 | let _ = windows_api.set_console_mode(handle, new_mode); |
622 | 0 | } |
623 | | |
624 | | /// Resolve cluster tags into hostnames |
625 | | /// |
626 | | /// Iterates over the list of hosts to find and resolve cluster tags. |
627 | | /// Nested cluster tags are supported but recursivness is not checked for. |
628 | | /// |
629 | | /// # Arguments |
630 | | /// |
631 | | /// * `hosts` - List of hosts including hostnames and or cluster tags |
632 | | /// * `clusters` - List of available cluster tags |
633 | | /// |
634 | | /// # Returns |
635 | | /// |
636 | | /// A list of hostnames |
637 | 6 | pub fn resolve_cluster_tags<'a>(hosts: Vec<&'a str>, clusters: &'a [Cluster]) -> Vec<&'a str> { |
638 | 6 | let mut resolved_hosts: Vec<&str> = Vec::new(); |
639 | | let mut is_cluster_tag: bool; |
640 | 19 | for host13 in hosts { |
641 | 13 | is_cluster_tag = false; |
642 | 27 | for cluster17 in clusters { |
643 | 17 | if host == cluster.name { |
644 | 3 | is_cluster_tag = true; |
645 | 3 | resolved_hosts.extend(resolve_cluster_tags( |
646 | 6 | cluster.hosts.iter()3 .map3 (|host| return &**host).collect3 (), |
647 | 3 | clusters, |
648 | | )); |
649 | 3 | break; |
650 | 14 | } |
651 | | } |
652 | 13 | if !is_cluster_tag { |
653 | 10 | resolved_hosts.push(host); |
654 | 10 | }3 |
655 | | } |
656 | 6 | return resolved_hosts; |
657 | 6 | } |
658 | | |
659 | | /// Launches a client console for each given host and waits for |
660 | | /// the client windows to exist before returning their handles. |
661 | | /// |
662 | | /// # Arguments |
663 | | /// |
664 | | /// * `windows_api` - The Windows API implementation to use |
665 | | /// * `hosts` - List of hosts |
666 | | /// * `username` - Optional username, if none is given |
667 | | /// the client will use the SSH config to |
668 | | /// determine a username. |
669 | | /// * `port` - Optional port for SSH connections |
670 | | /// * `debug` - Toggles debug mode on the client. |
671 | | /// * `workspace_area` - The available workspace area on the primary monitor |
672 | | /// minus the space occupied by the daemon console window. |
673 | | /// Used to arrange the client window. |
674 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
675 | | /// Used to arrange the client window. |
676 | | /// * `index_offset` - Offset used to position the new windows correctly |
677 | | /// from the start, avoiding flickering. |
678 | | /// |
679 | | /// # Returns |
680 | | /// |
681 | | /// A mapping from the order a client console window was launched at |
682 | | /// in relation to the other client windows and the clients console window handle. |
683 | 0 | async fn launch_clients<W: WindowsApi + 'static + Clone>( |
684 | 0 | windows_api: &W, |
685 | 0 | hosts: Vec<String>, |
686 | 0 | username: &Option<String>, |
687 | 0 | port: Option<u16>, |
688 | 0 | debug: bool, |
689 | 0 | workspace_area: &workspace::WorkspaceArea, |
690 | 0 | aspect_ratio_adjustment: f64, |
691 | 0 | index_offset: usize, |
692 | 0 | ) -> Vec<Client> { |
693 | 0 | let len_hosts = hosts.len(); |
694 | 0 | let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new(); |
695 | | |
696 | | // Create an Arc to share the windows_api across parallel tasks |
697 | 0 | let windows_api_arc = Arc::new(windows_api.clone()); |
698 | | |
699 | | // Create tasks for each client launch using spawn_blocking to handle the synchronous operations |
700 | 0 | let mut tasks = Vec::new(); |
701 | | |
702 | 0 | for (index, host) in hosts.into_iter().enumerate() { |
703 | 0 | let username_client = username.clone(); |
704 | 0 | let workspace_area_client = *workspace_area; |
705 | 0 | let windows_api_clone = Arc::clone(&windows_api_arc); |
706 | | |
707 | | // Use spawn_blocking to run the synchronous launch_client_console in parallel |
708 | 0 | let task = tokio::task::spawn_blocking(move || { |
709 | 0 | let (window_handle, process_handle) = launch_client_console( |
710 | 0 | windows_api_clone.as_ref(), |
711 | 0 | &host, |
712 | 0 | username_client, |
713 | 0 | port, |
714 | 0 | debug, |
715 | 0 | index + index_offset, |
716 | 0 | &workspace_area_client, |
717 | 0 | len_hosts + index_offset, |
718 | 0 | aspect_ratio_adjustment, |
719 | 0 | ); |
720 | 0 | return ( |
721 | 0 | index, |
722 | 0 | Client { |
723 | 0 | hostname: host, |
724 | 0 | window_handle, |
725 | 0 | process_handle, |
726 | 0 | }, |
727 | 0 | ); |
728 | 0 | }); |
729 | | |
730 | 0 | tasks.push(task); |
731 | | } |
732 | | |
733 | | // Wait for all tasks to complete in parallel |
734 | 0 | let mut results = Vec::new(); |
735 | 0 | for task in tasks { |
736 | 0 | match task.await { |
737 | 0 | Ok(result) => results.push(result), |
738 | 0 | Err(e) => panic!("Failed to launch client: {e}"), |
739 | | } |
740 | | } |
741 | | |
742 | | // Sort results by index to maintain order |
743 | 0 | results.sort_by_key(|(index, _)| return *index); |
744 | | |
745 | 0 | return results |
746 | 0 | .into_iter() |
747 | 0 | .map(|(_, client)| return client) |
748 | 0 | .collect(); |
749 | 0 | } |
750 | | |
751 | | /// Launchs a `client` console process with its own window with the given |
752 | | /// CLI arguments/options: `host`, `username`, `port`, `debug`. |
753 | | /// |
754 | | /// Waits for the window to open, then re-arranges it based on |
755 | | /// the total number of clients, the size of the daemon console window and |
756 | | /// its index relative to the other client windows. |
757 | | /// |
758 | | /// # Arguments |
759 | | /// |
760 | | /// * `windows_api` - The Windows API implementation to use |
761 | | /// * `host` - Hostname the client should connect to |
762 | | /// * `username` - Username the client should use |
763 | | /// * `port` - Optional port for SSH connections |
764 | | /// * `debug` - Toggle debug mode on the client |
765 | | /// * `index` - The index of the client in the list of all clients. |
766 | | /// Used to re-arrange the client window. |
767 | | /// * `workspace_area` - The available workspace area on the primary monitor |
768 | | /// minus the space occupied by the daemon console window. |
769 | | /// * `number_of_consoles` - The total number of active client console windows. |
770 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
771 | | /// |
772 | | /// # Returns |
773 | | /// |
774 | | /// A tuple containing the window handle and process handle of the client process. |
775 | 0 | fn launch_client_console<W: WindowsApi>( |
776 | 0 | windows_api: &W, |
777 | 0 | host: &str, |
778 | 0 | username: Option<String>, |
779 | 0 | port: Option<u16>, |
780 | 0 | debug: bool, |
781 | 0 | index: usize, |
782 | 0 | workspace_area: &workspace::WorkspaceArea, |
783 | 0 | number_of_consoles: usize, |
784 | 0 | aspect_ratio_adjustment: f64, |
785 | 0 | ) -> (HWND, HANDLE) { |
786 | | // The first argument must be `--` to ensure all following arguments are treated |
787 | | // as positional arguments and not as options if they start with `-`. |
788 | 0 | let mut client_args: Vec<String> = Vec::new(); |
789 | 0 | if debug { |
790 | 0 | client_args.push("-d".to_string()); |
791 | 0 | } |
792 | 0 | let mut actual_host = host; |
793 | 0 | let mut actual_username = username; |
794 | 0 | if let Some(split_result) = host.split_once("@") { |
795 | 0 | actual_username = Some(split_result.0.to_owned()); |
796 | 0 | actual_host = split_result.1; |
797 | 0 | } |
798 | 0 | if let Some(actual_username) = actual_username.as_deref() { |
799 | 0 | client_args.extend(vec!["-u".to_string(), actual_username.to_string()]); |
800 | 0 | } |
801 | 0 | if let Some(port) = port { |
802 | 0 | client_args.extend(vec!["-p".to_string(), port.to_string()]); |
803 | 0 | } |
804 | 0 | client_args.push("client".to_string()); |
805 | 0 | client_args.extend(vec!["--".to_string(), actual_host.to_string()]); |
806 | | |
807 | 0 | let process_info = spawn_console_process(windows_api, &format!("{PKG_NAME}.exe"), client_args) |
808 | 0 | .expect("Failed to create process"); |
809 | 0 | let client_window_handle = get_console_window_handle(windows_api, process_info.dwProcessId); |
810 | 0 | let process_handle = windows_api |
811 | 0 | .open_process(PROCESS_QUERY_INFORMATION.0, false, process_info.dwProcessId) |
812 | 0 | .unwrap_or_else(|err| { |
813 | 0 | panic!( |
814 | 0 | "Failed to open process handle for process {}: {}", |
815 | | process_info.dwProcessId, err |
816 | | ); |
817 | | }); |
818 | | |
819 | 0 | arrange_client_window( |
820 | 0 | windows_api, |
821 | 0 | &client_window_handle, |
822 | 0 | workspace_area, |
823 | 0 | index, |
824 | 0 | number_of_consoles, |
825 | 0 | aspect_ratio_adjustment, |
826 | | ); |
827 | 0 | return (client_window_handle, process_handle); |
828 | 0 | } |
829 | | |
830 | | /// Wait for the named pipe server to connect, then forward serialized |
831 | | /// input records read from the broadcast channel to the named pipe server. |
832 | | /// |
833 | | /// If writing to the pipe fails the pipe is closed and the routine ends. |
834 | | /// To detect if a client is still alive even if we are currently |
835 | | /// not sending data, we send a "keep alive packet", |
836 | | /// [`SERIALIZED_INPUT_RECORD_0_LENGTH`] bytes of `1`s. If that fails, the routine ends. |
837 | | /// |
838 | | /// # Arguments |
839 | | /// |
840 | | /// * `server` - The named pipe server over which we send data to the |
841 | | /// client. |
842 | | /// * `receiver` - The receiving end of the broadcast channel through |
843 | | /// which we get the serialize input records from the main |
844 | | /// thread that are to be sent to the client via the named |
845 | | /// pipe. |
846 | 2 | async fn named_pipe_server_routine( |
847 | 2 | server: NamedPipeServer, |
848 | 2 | receiver: &mut Receiver<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
849 | 2 | ) { |
850 | | // wait for a client to connect |
851 | 2 | server.connect().await.unwrap_or_else(|err| {0 |
852 | 0 | error!("{}", err); |
853 | 0 | panic!("Timeded out waiting for clients to connect to named pipe server",) |
854 | | }); |
855 | | loop { |
856 | 11 | let ser_input_record8 = match receiver.try_recv() { |
857 | 8 | Ok(val) => val, |
858 | | Err(TryRecvError::Empty) => { |
859 | 2 | tokio::time::sleep(Duration::from_millis(5)).await; |
860 | | // Try sending dummy data to detect early if the pipe is closed because the client exited |
861 | 2 | match server.try_write(&[u8::MAX; SERIALIZED_INPUT_RECORD_0_LENGTH]) { |
862 | 2 | Ok(_) => continue, |
863 | 0 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue, |
864 | | Err(_) => { |
865 | 0 | debug!( |
866 | 0 | "Named pipe server ({:?}) is closed, stopping named pipe server routine", |
867 | | server |
868 | | ); |
869 | 0 | return; |
870 | | } |
871 | | } |
872 | | } |
873 | 1 | Err(err) => { |
874 | 1 | error!("{}"0 , err); |
875 | 1 | panic!("Failed to receive data from the Receiver"); |
876 | | } |
877 | | }; |
878 | | loop { |
879 | 14 | server.writable().await.unwrap_or_else(|err| {0 |
880 | 0 | error!("{}", err); |
881 | 0 | panic!("Timed out waiting for named pipe server to become writable",) |
882 | | }); |
883 | 14 | match server.try_write(&ser_input_record) { |
884 | | Ok(SERIALIZED_INPUT_RECORD_0_LENGTH) => { |
885 | 7 | debug!("Successfully written all data"0 ); |
886 | 7 | break; |
887 | | } |
888 | 0 | Ok(n) => { |
889 | | // The data was only written partially, try again |
890 | 0 | warn!( |
891 | 0 | "Partially written data, expected {} but only wrote {}", |
892 | | SERIALIZED_INPUT_RECORD_0_LENGTH, n |
893 | | ); |
894 | 0 | continue; |
895 | | } |
896 | 7 | Err(e6 ) if e.kind() == io::ErrorKind::WouldBlock6 => { |
897 | | // Try again |
898 | 6 | debug!("Writing to named pipe server would have blocked"0 ); |
899 | 6 | continue; |
900 | | } |
901 | | Err(_) => { |
902 | | // Can happen if the pipe is closed because the |
903 | | // client exited |
904 | 1 | debug!( |
905 | 0 | "Named pipe server ({:?}) is closed, stopping named pipe server routine", |
906 | | server |
907 | | ); |
908 | 1 | return; |
909 | | } |
910 | | } |
911 | | } |
912 | | } |
913 | 1 | } |
914 | | |
915 | | /// Re-sizes and re-positions the given client window based on the total number of clients, |
916 | | /// the size of the daemon console window and its index relative to the other client windows. |
917 | | /// |
918 | | /// # Arguments |
919 | | /// |
920 | | /// * `windows_api` - The Windows API implementation to use |
921 | | /// * `handle` - Reference the windows handle of a client console window. |
922 | | /// * `workspace_area` - The available workspace area on the primary monitor |
923 | | /// minus the space occupied by the daemon console window. |
924 | | /// * `index` - The index of the client in the list of all clients. |
925 | | /// * `number_of_consoles` - The total number of active client console windows. |
926 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
927 | 0 | fn arrange_client_window<W: WindowsApi>( |
928 | 0 | windows_api: &W, |
929 | 0 | handle: &HWND, |
930 | 0 | workspace_area: &workspace::WorkspaceArea, |
931 | 0 | index: usize, |
932 | 0 | number_of_consoles: usize, |
933 | 0 | aspect_ratio_adjustment: f64, |
934 | 0 | ) { |
935 | 0 | let (x, y, width, height) = determine_client_spatial_attributes( |
936 | 0 | index as i32, |
937 | 0 | number_of_consoles as i32, |
938 | 0 | workspace_area, |
939 | 0 | aspect_ratio_adjustment, |
940 | 0 | ); |
941 | | // Since windows update 10.0.19041.5072 it can happen that a client windows rendering is broken |
942 | | // after a move+resize. Why is unclear, but resizing again does solve the issue. |
943 | | // We first make the window 1 pixel in each dimension too small and imediately fix it. |
944 | | // To reduce overhead we do not repaint the window the first time. |
945 | 0 | windows_api |
946 | 0 | .move_window(*handle, x, y, width - 1, height - 1, false) |
947 | 0 | .unwrap_or_else(|err| { |
948 | 0 | error!("{}", err); |
949 | 0 | panic!("Failed to move window",) |
950 | | }); |
951 | 0 | windows_api |
952 | 0 | .move_window(*handle, x, y, width, height, true) |
953 | 0 | .unwrap_or_else(|err| { |
954 | 0 | error!("{}", err); |
955 | 0 | panic!("Failed to move window",) |
956 | | }); |
957 | 0 | } |
958 | | |
959 | | /// Calculates the position and dimensions for a client window given its index, |
960 | | /// the total number of clients and the `aspect_ratio_adjustment` daemon configuration. |
961 | | /// |
962 | | /// # Arguments |
963 | | /// |
964 | | /// * `index` - The index of the client in the list of all clients. |
965 | | /// * `number_of_consoles` - The total number of active client console windows. |
966 | | /// * `workspace_area` - The available workspace area on the primary monitor |
967 | | /// minus the space occupied by the daemon console window. |
968 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
969 | | /// * `> 0.0` - Aims for vertical rectangle shape. |
970 | | /// The larger the value, the more exaggerated the "verticality". |
971 | | /// Eventually the windows will all be columns. |
972 | | /// * `= 0.0` - Aims for square shape. |
973 | | /// * `< 0.0` - Aims for horizontal rectangle shape. |
974 | | /// The smaller the value, the more exaggerated the "horizontality". |
975 | | /// Eventually the windows will all be rows. |
976 | | /// `-1.0` is the sweetspot for mostly preserving a 16:9 ratio. |
977 | 0 | fn determine_client_spatial_attributes( |
978 | 0 | index: i32, |
979 | 0 | number_of_consoles: i32, |
980 | 0 | workspace_area: &workspace::WorkspaceArea, |
981 | 0 | aspect_ratio_adjustment: f64, |
982 | 0 | ) -> (i32, i32, i32, i32) { |
983 | 0 | let aspect_ratio = (workspace_area.width |
984 | 0 | + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) * 2) |
985 | 0 | as f64 |
986 | 0 | / (workspace_area.height + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * 2) |
987 | 0 | as f64; |
988 | | |
989 | 0 | let grid_columns = max( |
990 | 0 | ((number_of_consoles as f64).sqrt() * (aspect_ratio + aspect_ratio_adjustment)) as i32, |
991 | | 1, |
992 | | ); |
993 | 0 | let grid_rows = max( |
994 | 0 | (number_of_consoles as f64 / grid_columns as f64).ceil() as i32, |
995 | | 1, |
996 | | ); |
997 | | |
998 | 0 | let grid_column_index = index % grid_columns; |
999 | 0 | let grid_row_index = index / grid_columns; |
1000 | | |
1001 | 0 | let is_last_row = grid_row_index == grid_rows - 1; |
1002 | 0 | let last_row_console_count = number_of_consoles % grid_columns; |
1003 | | |
1004 | 0 | let console_width = if is_last_row && last_row_console_count != 0 { |
1005 | 0 | (workspace_area.width / last_row_console_count) |
1006 | 0 | + if last_row_console_count > 1 { |
1007 | 0 | workspace_area.x_fixed_frame + workspace_area.x_size_frame |
1008 | | } else { |
1009 | 0 | 0 |
1010 | | } |
1011 | | } else { |
1012 | 0 | (workspace_area.width / grid_columns) |
1013 | 0 | + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) |
1014 | | }; |
1015 | | |
1016 | 0 | let console_height = (workspace_area.height |
1017 | 0 | + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * grid_row_index) |
1018 | 0 | / grid_rows; |
1019 | | |
1020 | 0 | let x = grid_column_index * console_width |
1021 | 0 | - ((workspace_area.x_fixed_frame + workspace_area.x_size_frame) * (grid_column_index + 1)); |
1022 | 0 | let y = grid_row_index * console_height |
1023 | 0 | - ((workspace_area.y_fixed_frame + workspace_area.y_size_frame) * (grid_row_index - 1)); |
1024 | | |
1025 | 0 | return get_console_rect(x, y, console_width, console_height, workspace_area); |
1026 | 0 | } |
1027 | | |
1028 | | /// Transform the position and dimensions of a console window based |
1029 | | /// on the workspace area. |
1030 | | /// |
1031 | | /// To minimize empty space between windows, width and height must be adjusted |
1032 | | /// by the `fixed_frame` and `size_frame` values. |
1033 | | /// |
1034 | | /// # Arguments |
1035 | | /// |
1036 | | /// * `x` - The `x` coordinate of the window. |
1037 | | /// * `y` - The `y` coordinate of the window. |
1038 | | /// * `width` - The `width` in pixels of the window. |
1039 | | /// * `height` - The `height` in pixels of the window. |
1040 | | /// * `workspace_area` - The available workspace area on the primary monitor minus |
1041 | | /// the space occupied by the daemon console window. |
1042 | | /// |
1043 | | /// # Returns |
1044 | | /// |
1045 | | /// (`x`, `y`, `width`, `height`) |
1046 | | /// |
1047 | 0 | fn get_console_rect( |
1048 | 0 | x: i32, |
1049 | 0 | y: i32, |
1050 | 0 | width: i32, |
1051 | 0 | height: i32, |
1052 | 0 | workspace_area: &workspace::WorkspaceArea, |
1053 | 0 | ) -> (i32, i32, i32, i32) { |
1054 | 0 | return ( |
1055 | 0 | std::cmp::max( |
1056 | 0 | workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame), |
1057 | 0 | workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame) + x, |
1058 | 0 | ), |
1059 | 0 | workspace_area.y - (workspace_area.y_fixed_frame + workspace_area.y_size_frame) + y, |
1060 | 0 | std::cmp::min(workspace_area.width, width), |
1061 | 0 | height, |
1062 | 0 | ); |
1063 | 0 | } |
1064 | | |
1065 | | /// Spawns a background thread that ensures the z-order of all client |
1066 | | /// windows is in sync with the daemon window. |
1067 | | /// I.e. if the daemon window is focussed, all clients should be moved to the foreground. |
1068 | | /// |
1069 | | /// # Arguments |
1070 | | /// |
1071 | | /// * `windows_api` - Arc-wrapped Windows API implementation for thread-safe access |
1072 | | /// * `clients` - A thread safe mapping from the number |
1073 | | /// a client console window was launched at |
1074 | | /// in relation to the other client windows |
1075 | | /// and the clients console window handle. |
1076 | | /// The mapping must be thread safe to allow |
1077 | | /// it to be modified by the main thread |
1078 | | /// while we periodically read from it in the |
1079 | | /// background thread. |
1080 | 0 | fn ensure_client_z_order_in_sync_with_daemon<W: WindowsApi + Send + Sync + 'static>( |
1081 | 0 | windows_api: Arc<W>, |
1082 | 0 | clients: Arc<Mutex<Vec<Client>>>, |
1083 | 0 | ) { |
1084 | 0 | tokio::spawn(async move { |
1085 | 0 | let daemon_handle = get_console_window_wrapper(windows_api.as_ref()); |
1086 | 0 | let mut previous_foreground_window = get_foreground_window_wrapper(windows_api.as_ref()); |
1087 | | loop { |
1088 | 0 | tokio::time::sleep(Duration::from_millis(1)).await; |
1089 | 0 | let foreground_window = get_foreground_window_wrapper(windows_api.as_ref()); |
1090 | 0 | if previous_foreground_window == foreground_window { |
1091 | 0 | continue; |
1092 | 0 | } |
1093 | 0 | if foreground_window == daemon_handle |
1094 | 0 | && !clients.lock().unwrap().iter().any(|client| { |
1095 | 0 | return client.window_handle == previous_foreground_window.hwdn |
1096 | 0 | || client.window_handle == daemon_handle.hwdn; |
1097 | 0 | }) |
1098 | 0 | { |
1099 | 0 | defer_windows( |
1100 | 0 | windows_api.as_ref(), |
1101 | 0 | &clients.lock().unwrap(), |
1102 | 0 | &daemon_handle.hwdn, |
1103 | 0 | ); |
1104 | 0 | } |
1105 | 0 | previous_foreground_window = foreground_window; |
1106 | | } |
1107 | | }); |
1108 | 0 | } |
1109 | | |
1110 | | /// Move all given windows to the foreground. |
1111 | | /// |
1112 | | /// Restores minimized windows. |
1113 | | /// If a window handle no longer points to a valid window, it is skipped. |
1114 | | /// The daemon window is deferred last and receives focus. |
1115 | | /// |
1116 | | /// # Arguments |
1117 | | /// |
1118 | | /// * `windows_api` - The Windows API implementation to use |
1119 | | /// * `clients` - A thread safe mapping from the number |
1120 | | /// a client console window was launched at |
1121 | | /// in relation to the other client windows |
1122 | | /// and the clients console window handle. |
1123 | | /// * `daemon_handle` - Handle to the daemon console window. |
1124 | 0 | fn defer_windows<W: WindowsApi>(windows_api: &W, clients: &[Client], daemon_handle: &HWND) { |
1125 | 0 | for client in clients.iter().chain([&Client { |
1126 | 0 | hostname: "root".to_owned(), |
1127 | 0 | window_handle: *daemon_handle, |
1128 | 0 | process_handle: HANDLE::default(), |
1129 | 0 | }]) { |
1130 | 0 | let placement = match windows_api.get_window_placement(client.window_handle) { |
1131 | 0 | Ok(placement) => placement, |
1132 | | Err(_) => { |
1133 | 0 | continue; |
1134 | | } |
1135 | | }; |
1136 | | // First restore if window is minimized |
1137 | 0 | if placement.showCmd == SW_SHOWMINIMIZED.0.try_into().unwrap() { |
1138 | 0 | let _ = windows_api.show_window(client.window_handle, SW_RESTORE); |
1139 | 0 | } |
1140 | | // Then bring it to front using UI automation |
1141 | 0 | let _ = windows_api.focus_window_with_automation(client.window_handle); |
1142 | | } |
1143 | 0 | } |
1144 | | |
1145 | | /// The entrypoint for the `daemon` subcommand. |
1146 | | /// |
1147 | | /// Spawns 1 client process with its own window for each host |
1148 | | /// and 1 worker thread that handles communication with the client |
1149 | | /// over a named pipe. |
1150 | | /// Responsible for client window positioning and sizing. |
1151 | | /// Handles control mode. |
1152 | | /// Main thread reads input records from the console input buffer |
1153 | | /// and propagates them via the background threads to all clients |
1154 | | /// simultaneously. |
1155 | | /// |
1156 | | /// # Arguments |
1157 | | /// |
1158 | | /// * `windows_api` - The Windows API implementation to use |
1159 | | /// * `hosts` - List of hostnames for which to launch clients. |
1160 | | /// * `username` - Username used to connect to the hosts. |
1161 | | /// If none, each client will use the SSH config to determine |
1162 | | /// a suitable username for their respective host. |
1163 | | /// * `port` - Optional port used for all SSH connections. |
1164 | | /// * `config` - The `DaemonConfig`. |
1165 | | /// * `debug` - Enables debug logging |
1166 | 0 | pub async fn main<W: WindowsApi + Clone + 'static>( |
1167 | 0 | windows_api: &W, |
1168 | 0 | hosts: Vec<String>, |
1169 | 0 | username: Option<String>, |
1170 | 0 | port: Option<u16>, |
1171 | 0 | config: &DaemonConfig, |
1172 | 0 | clusters: &[Cluster], |
1173 | 0 | debug: bool, |
1174 | 0 | ) { |
1175 | 0 | let daemon: Daemon = Daemon { |
1176 | 0 | hosts: explode(&hosts.join(" ")).unwrap_or(hosts), |
1177 | 0 | username, |
1178 | 0 | port, |
1179 | 0 | config, |
1180 | 0 | clusters, |
1181 | 0 | control_mode_state: ControlModeState::Inactive, |
1182 | 0 | debug, |
1183 | 0 | }; |
1184 | 0 | daemon.launch(windows_api).await; |
1185 | 0 | debug!("Actually exiting"); |
1186 | 0 | } |
1187 | | |
1188 | | #[cfg(test)] |
1189 | | #[path = "../tests/daemon/test_mod.rs"] |
1190 | | mod test_mod; |